ssunguotu

  • 主页
  • 随笔
所有文章 关于我

ssunguotu

  • 主页
  • 随笔

[C++]拷贝构造函数引发的血案

2019-11-21

一个因拷贝构造函数引发的小bug

拷贝构造函数引发的血案


1. 拷贝构造函数引发的血案

女票在写链表的练习题时,出现了这样的一个bug:调用某个函数前,一切都是正常的;调用后,发现引发了异常。

然后我想着肯定是这个函数实现的问题呀,便开始逐步调试看看是哪里引发了异常。发现是析构函数引发了异常。恩,有点奇怪,为什么之前写的那么多函数都没问题呢?

然后就发现了一个神奇的现象:调用某个函数前,new出来的数据是存在的,调用这个函数后,这个数据就不存在了,然而我们参数是传值进去的,为什么呢?

在往常的理解中,传值进去应该是不会影响到这个数据的呀,那是为什么呢。

先来看一段简单的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

class Test {
public:
int *data;
int dataSize;
Test(int maxn) {
dataSize = maxn;
data = new int[maxn];
}
~Test() {
delete data;
}
};

void test(Test tmp) {

}

int main()
{
Test obj{ 2 };
obj.data[0] = 1;
test(obj);
}

这是我们打断点看它在这里data的信息:

1570765877972

然后调用test函数之后:

1570765969812

data数据就没了。。

其实原因也很简单,也并不难想到。就是因为我们的类成员中有指针,所以我们进行值传递的时候默认拷贝构造函数会把指针也赋值给函数中的局部变量。局部变量在参数调用结束后就析构掉了,我们写的析构函数就把这个指针给delete掉了…

发现bug在哪儿之后就发现…前人总结的经验还是要听啊,不深拷贝这不就出问题了嘛!

所以总结一下就是:如果成员变量里有指针的话,一定要记得深拷贝!!!!

2. 递归调用构造函数引发的第二个血案

班里的哥们问完我可变参数的问题后,他自己的实现出了问题,他是直接把构造函数递归调用了。

是因为构造函数并不完全等同于普通的函数…所以出了问题。

他给我看了一下他写的测试代码:

1570770617633

这个问题…上学期实验室笔试的时候遇见过,还错了= = 然鹅并没有长记性,当时就没搞懂是为啥,这次又遇见了。

所以为什么n不是0呢?

因为构造函数不同于普通函数。

我们知道,当定义一个对象时,会按顺序做2件事情:

  1. 分配好内存(非静态数据成员是未初始化的)
  2. 调用构造函数(构造函数的本意就是初始化非静态数据成员)

显然上面代码中,My b(4);这里已经为b分配了内存,然后调用默认构造函数,但是默认构造函数还未执行完,却调用了另一个构造函数,这样相当于产生了一个匿名的临时My对象,然后对这个临时对象的n进行了赋值,所以我们递归最外层的n仍是4。

最后总结:

  1. 在构造函数里调用另一个构造函数,并不是让第二个构造函数在第一次分配好的内存上执行,而是分配新的内存。
  2. 如果希望可以在构造函数中实现一个递归的算法,可以另写一个成员函数并在那个函数里递归。

3. 可变模板参数

自定义类如何实现像一个STL容器一样,这样初始一个变量呢:

1
vector<int> vec{1,2,3};

实际上有很多种实现方法,可以调用库,也可以使用initializer_list(初始化列表类,这个很好用也很简单),还有就是我使用的可变参数模板。

这里分享一篇非常好的文章,讲的很详细:https://www.cnblogs.com/qicosmos/p/4325949.html

C++11的新特性–可变模版参数(variadic templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。

声明模板函数

可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号“…”。比如我们常常这样声明一个可变模版参数:template<typename…>或者template<class…>,一个典型的可变模版参数的定义是这样的:

1
2
3
4
5
6
7
8
9
10
11
template <class... T>
void f(T... args)
{
cout << sizeof...(args) << endl; //打印变参的个数
}

f(); //0
f(1, 2); //2
f(1, 2.5, ""); //3

//sizeof...:打印参数包中单个参数的个数。

省略号的作用有两个:

  1. 声明一个参数包 T… args,这个参数包中可以包含0到任意个模板参数。(声明和定义函数体时)
  2. 在模板函数的右边,可以将参数包展开成一个一个独立的参数。(展开参数包时)

还有,省略号都是在右边的。

展开参数包

有两种比较常见的展开方式:1. 递归函数方式展开参数包 2. 逗号表达式展开参数包

递归展开

下面的代码是用的递归方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#pragma once
template<class T>
class SeqList
{
private:
T* data = nullptr;
int last = -1;

//函数
template<typename ...Args>
void insert(const T& arg1) {
data[++last] = arg1;
}

template<typename ...Args>
void insert(const T& arg1, const Args... args) {
data[++last] = arg1;
insert(args...);
}
public:
template<typename ...Args>
SeqList(const Args... args) {
data = new T[sizeof...(args)];
insert(args...);
}
void show() {
for (int i = 0; i <= last; i++) {
std::cout << data[i] << ' ';
}
}
};

通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。

参数包展开函数:

1
2
3
4
5
template<typename ...Args>
void insert(const T& arg1, const Args... args) {
data[++last] = arg1;
insert(args...);
}

递归终止函数:

1
2
3
4
template<typename ...Args>
void insert(const T& arg1) {
data[++last] = arg1;
}

这里注意一点:函数在接受参数包时,如果形参为普通参数,会从参数包中取出第一个元素赋给它,并在实参参数包中将其删除掉;如果形参为参数包,就会把整个实参参数包赋给形参。并且在这个参数的后面不可以再继续写参数了,会报错。

所以过程就是:展开函数有两个参数,第一个参数是一个普通参数,这样会使得参数包的大小减一;第二个参数是一个参数包,把规模已经减小的它传给自身函数,继续递归调用。

这样就使得规模不断减小,直到规模为1,进入递归终止函数。

逗号表达式展开

代码:

1
2
3
4
5
6
7
8
9
10
template<typename... Args>
void insert(const T& arg1) {
data[++last] = arg1;
}

template<typename... Args>
SeqList(const Args... args) {
data = new T[sizeof...(args)];
int arr[] = { (insert(args), 0)... };
}

这样写的好处是没有递归,迭代实现使得效率更高,而且也不用写递归终止函数。

我们来理解一下逗号表达式:

从左往右逐个计算表达式,整个表达式的值为最后一个表达式的值。

就是说:(insert(args), 0) 这段代码,无论前面的函数返回值是什么,整个括号的值始终为0。

所以最后数组中的元素也全是零。

注意:

  1. 省略号在这里的作用是将参数包展开成一个一个的参数。
  2. 这种写法只可用列表初始化的方式完成。
赏

谢谢你请我吃糖果

扫一扫,分享到微信

微信分享二维码
[C++]尾递归及迭代器失效问题
[数据结构]Bloomm-Filter实现及思路
  1. 1. 拷贝构造函数引发的血案
    1. 1.1. 1. 拷贝构造函数引发的血案
    2. 1.2. 2. 递归调用构造函数引发的第二个血案
    3. 1.3. 3. 可变模板参数
      1. 1.3.1. 声明模板函数
      2. 1.3.2. 展开参数包
        1. 1.3.2.1. 递归展开
        2. 1.3.2.2. 逗号表达式展开
© 2021 ssunguotu
Hexo Theme Yilia by Litten
  • 所有文章
  • 关于我

tag:

  • hexo生成错误
  • 数据结构
  • <数据结构>
  • 爬虫
  • tst
  • 算法题
  • A*搜索

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

没啥东西。